ext.define('extension.sync', function() {

var utils = extension.utils;
var messages = extension.messages;
var storage = extension.storage;
var data = Object.create(null);
var defaults = Object.freeze({
    'sync.enabled': 0,
    'sync.filters': 1,
    'sync.settings': 1
});
var queue = utils.opqueue(storage.UNIT, storage.OPS_PER_UNIT);
queue.tick = Date.now();

function onChange(o) {
    if (getPref('sync.enabled')) {
        var d = filterDiff(o);
        sendChanges(d);
    }
}

function sanitize(a) {
    if (a == null) return '';
    return JSON.stringify(a).replace(/[\u0000-\u001F\u0080-\uFFFF\u007F\u003C\u003E]/g, function(c) {
        return '\\u' + ('0000' + c.charCodeAt(0).toString(16)).slice(-4);
    });
}

function debug(str) {
    if (typeof str === 'string' ? str : chrome.runtime.lastError)
        console.warn('[%s] %s', utils.time(), str || chrome.runtime.lastError.message);
}

function setRemote(c) {
    queue.put(function() {
        storage.local.get(Object.keys(c), function(o) {
            var d = {};
            for (var k in o) {
                if (k.length + sanitize(o[k]).length > storage.BYTES_PER_ITEM) {
                    debug('can\'t sync ' + k + ': item too large');
                    continue;
                }
                d[k] = o[k];
            }
            storage.remote.set(d, debug);
        });
    });
}

function removeRemote(c) {
    queue.put(function() {
        storage.local.get(c, function(o) {
            var d = [];
            for (var i = 0; i < c.length; i++) {
                if (!(c[i] in o))
                    d.push(c[i]);
            }
            storage.remote.remove(d, debug);
        });
    });
}

function filterPattern() {
    var f = ['(?!sync)'];
    if (getPref('sync.settings'))
        f.push('prefs');
    if (getPref('sync.filters'))
        f.push('data');
    return '/^(' + f.join('|') + ')\\./';
}

function filterChanges(o) {
    var d = {}, temp = filterData(o);
    for (var k in temp)
        d[k] = temp[k].newValue;
	return d;
}

function filterDiff(o) {
    var f = filterPattern();
    return {
        added: utils.grep(f, o.added),
        removed: utils.grep(f, o.removed)
    };
}

function filterData(o) {
    return utils.grep(filterPattern(), o);
}

function updateAccount(o) {
    if (Object.keys(o).length)
        messages.send('account-data', o);
}

function sendChanges(o) {
    if (Object.keys(o.added).length)
        setRemote(o.added);
    if (o.removed.length)
        removeRemote(o.removed);
}

function retreiveChanges(o) {
    var keys = Object.keys(o);
    if (keys.length)
        storage.remote.get(keys, updateAccount);
}

function getPref(o) {
    if (typeof o === 'string')
        return data[o] != null ? data[o] : defaults[o];
    return utils.scan(data, defaults);
}

function setPref(o) {
    var p = utils.object(arguments[0], arguments[1]), changes = {};
    for (var k in p) {
        if (k in defaults) {
            var c = {};
            if (k in data) {
                if (utils.equals(p[k], data[k]))
                    continue;
                c.oldValue = data[k];
            }
            if (p[k] == null || utils.equals(p[k], defaults[k])) {
                if (data[k] != null)
                    c.oldValue = data[k];
                delete data[k];
            }
            else
                c.newValue = data[k] = p[k];
            if ('oldValue' in c || 'newValue' in c)
                changes[k] = c;
        }
    }
    if (Object.keys(changes).length) {
        changes = utils.diff(changes, getPref());
        messages.send('change-settings', changes);
    }
}

function pullRemote(o) {
    if (getPref('sync.enabled')) {
        var d = filterData(o);
        retreiveChanges(d);
    }
}

function pushRemote(o) {
    if (getPref('sync.enabled')) {
        var d = filterData(o);
        sendChanges({ added: d, removed: [] });
    }
}

function mergeAccount(o) {
    if (getPref('sync.enabled')) {
        var d = filterChanges(o);
        updateAccount(d);
    }
}

return {
    bind: function() {
        messages.listen({
            'change-settings': onChange
        });
    },
    get: getPref,
    set: setPref,
    pull: pullRemote,
    push: pushRemote,
    merge: mergeAccount
};

});
